<template>
  <div id="imgMigration">
    <el-collapse v-model="activeCollName">
      <el-collapse-item name="connInfo">
        <template slot="title">
          <span class="coll-title-span">
            基础信息<i class="header-icon el-icon-info" />
          </span>
        </template>
        <div class="conn-div">
          <el-tabs v-model="infoTab" type="border-card">
            <template v-for="item in tabsArr">
              <el-tab-pane
                v-if="item.name === 'sql_conn' ? sqlConv :
                  (item.name === 'src_dst_conn' || item.name === 'list_conn') ? !sqlConv : true"
                :key="item.name"
                :label="item.name"
                :name="item.name"
              >
                <span slot="label">
                  {{ item.label }}
                  <el-tooltip
                    class="item"
                    effect="dark"
                    placement="right-start"
                  >
                    <div slot="content" v-html="item.tip" />
                    <i :class="item.tip? 'el-icon-question' : ''" />
                  </el-tooltip>
                </span>
                <div class="conn-container">
                  <template v-for="dItem in item.compEls['div-els']">
                    <div
                      :key="dItem.id"
                      :class="dItem.class"
                    >
                      <!-- 源环境和目标环境分块 -->
                      <div
                        v-if="dItem.class === 'conn-div'"
                        style="display: flex; gap: 10px; width: 100%; align-items: center"
                      >
                        <span class="trans-span">
                          {{ dItem.desc }}
                        </span>
                        <div v-if="dItem.sqlDiv" style="display: flex; gap: 10px; width: 100%; align-items: center">
                          <el-button
                            v-if="dItem.id === 'tar_conn'"
                            size="mini"
                            type="text"
                            icon="el-icon-download"
                            style="flex: 0 1 auto; margin: 0 auto;"
                            @click="exportSqlFile"
                          >导出SQL文件</el-button>
                          <div
                            v-if="dItem.id === 'res_conn'"
                            style="display: flex;
                            margin: 0 auto;
                            flex-wrap: wrap;
                            align-items: center;
                            gap: 8px;
                            flex: 1;
                            min-width: 0"
                          >
                            <span style="white-space: nowrap;">
                              <i style="color:#F56C6C">*</i> 目标靶场ID：
                            </span>
                            <el-input
                              v-model="sqlRangeId"
                              clearable
                              placeholder="请输入目标靶场ID"
                              suffix-icon="el-icon-folder"
                              size="mini"
                              style="flex: 1; min-width: 120px;"
                            />
                          </div>
                        </div>
                        <div v-else style="display: flex; gap: 10px; width: 100%; align-items: center">
                          <el-checkbox
                            style="flex: 0 1 auto; margin: 0 auto;"
                            :value="getDeepDefValue(envAliasInfo, [dItem.id, 'disTypeFrom'], false)"
                            @change="updateDeepValue(envAliasInfo, [dItem.id, 'disTypeFrom'], $event)"
                          >区分来源</el-checkbox>
                          <el-autocomplete
                            style="flex: 0 1 auto;margin: 0 auto; "
                            popper-class="env-alias-class"
                            size="mini"
                            :value="getDeepDefValue(envAliasInfo, [dItem.id, 'envAlias'], '')"
                            :fetch-suggestions="(queryStr, cb) => { loadPreEnvList(queryStr, cb, dItem.id) }"
                            placeholder="请输入环境别名"
                            @input="updateDeepValue(envAliasInfo, [dItem.id, 'envAlias'], $event)"
                            @select="handlePreEnvSelect(dItem.id, $event)"
                          >
                            <i
                              slot="suffix"
                              class="el-icon-more el-input__icon"
                              @click="showMoreEnvs(dItem.id)"
                            />
                            <template slot-scope="{ item }">
                              <div class="name">{{ item.envAlias ? item.envAlias :
                                item.normal ? item.normal.host :
                                item.appDb ? item.appDb.host:
                                item.osDb ? item.osDb.host:
                                item.ssh ? item.ssh.host : '未知环境'
                              }}</div>
                              <span class="addr">{{ item.normal ? item.normal.host + ':' + item.normal.port:
                                item.appDb ? item.appDb.host + ':' + item.appDb.port:
                                item.osDb ? item.osDb.host + ':' + item.osDb.port:
                                item.ssh ? item.ssh.host + ':' + item.ssh.port : '未知环境'
                              }}</span>
                            </template>
                          </el-autocomplete>
                        </div>
                        <el-checkbox
                          v-if="(dItem.id === 'src_conn' || dItem.id === 'dst_conn') ? dItem.showOld && migData : sqlConv"
                          style="flex: 0 1 auto; margin-left: auto;"
                          :value="getDeepDefValue(connForm, [dItem.id, 'normal', 'isOldEnv'], false)"
                          @change="updateDeepValue(connForm, [dItem.id, 'normal', 'isOldEnv'], $event)"
                        >老极智环境</el-checkbox>
                      </div>

                      <!-- 不需要分块的div内容 -->
                      <el-form
                        v-if="dItem.class === 'one-div'"
                        :ref="dItem.id + '_' + 'formRef'"
                        :model="getDeepDefValue(imgProjectInfo, [dItem.id], {})"
                        :rules="getOneFormRules() || {}"
                        label-width="120px"
                        size="mini"
                        :disabled="(!migImg && !migData && !sqlConv) || isMigActing"
                      >
                        <el-form-item
                          v-for="formItem in item.compEls['form-item-els']"
                          :key="formItem.prop"
                          :label="formItem.label"
                          :prop="formItem.prop"
                        >
                          <el-input
                            clearable
                            :placeholder="formItem.placeholder"
                            :type="formItem.type || undefined"
                            :rows="formItem.rows"
                            :maxlength="formItem.maxLength"
                            :autosize="false"
                            :value="getDeepValue(imgProjectInfo, [dItem.id, formItem.prop])"
                            :disabled="(formItem.isRefer && !migData) || (formItem.isPath && formItem.prop === 'srcPath' && !migImg)"
                            @input="updateDeepValue(imgProjectInfo, [dItem.id, formItem.prop], $event)"
                          />
                        </el-form-item>
                      </el-form>

                      <!-- 需要分块的环境信息内容 -->
                      <div v-else>
                        <el-divider v-if="dItem.class !== 'line-div'" />
                        <div v-if="dItem.class === 'conn-div'">
                          <div v-if="!item.compEls['common-tabs']">
                            <template v-for="formItem in item.compEls['form-item-els']">
                              <CodeEditor
                                :key="formItem.prop"
                                :ref="`${dItem.id}_dataRef`"
                                clearable
                                :rows="formItem.rows"
                                :placeholder="dItem.id === 'res_conn'? formItem.placeholder : '转换后SQL，一行一条'"
                                :readonly="dItem.id === 'tar_conn'"
                                :value="getDeepDefValue(sqlForm, [dItem.id, formItem.prop])"
                                @input="updateDeepValue(sqlForm, [dItem.id, formItem.prop], $event)"
                              />
                            </template>
                          </div>
                          <el-tabs
                            v-else
                            v-model="connActiveName[dItem.id]"
                            @tab-click="handleEnvTabClick"
                          >
                            <template v-for="eItem in item.compEls['common-tabs']">
                              <el-tab-pane
                                v-if="eItem.isShow || migData"
                                :key="eItem.name"
                                :label="eItem.label"
                                :name="dItem.id + '_' + eItem.name"
                              >
                                <el-checkbox
                                  v-if="eItem.showSwitch"
                                  :value="getDeepDefValue(connForm, [dItem.id, eItem.name, 'enable' + eItem.name], eItem.defVal)"
                                  style="padding: 10px"
                                  @change="dbEnvEnable(dItem.id, eItem.name, $event)"
                                >启用环境</el-checkbox>
                                <el-form
                                  :ref="`${dItem.id}-${eItem.name}-formRef`"
                                  :rules="getDynamicRules(dItem.id, eItem.name)"
                                  :model="getDeepDefValue(connForm, [dItem.id, eItem.name], {})"
                                  label-width="80px"
                                  size="mini"
                                  :disabled="(!migImg && !migData && !sqlConv) || isMigActing ||
                                    (eItem.showSwitch && !getDeepDefValue(connForm, [dItem.id, eItem.name, 'enable' + eItem.name]))"
                                >
                                  <el-form-item
                                    v-for="formItem in item.compEls['form-item-els']"
                                    :key="formItem.prop"
                                    :label="formItem.label"
                                    :prop="formItem.prop"
                                  >
                                    <el-input
                                      clearable
                                      :show-password="formItem.prop === 'password'"
                                      :max="formItem.max"
                                      :maxlength="formItem.maxLength"
                                      :placeholder="formItem.placeholder"
                                      :value="formItem.prop === 'port'?
                                        getDeepDefValue(connForm, [dItem.id, eItem.name, formItem.prop], '22')
                                        : getDeepValue(connForm, [dItem.id, eItem.name, formItem.prop])"
                                      @input="updateDeepValue(connForm, [dItem.id, eItem.name, formItem.prop], $event)"
                                    />
                                  </el-form-item>
                                </el-form>
                              </el-tab-pane>
                            </template>
                          </el-tabs>
                        </div>
                      </div>
                    </div>
                  </template>
                </div>
              </el-tab-pane>
            </template>
          </el-tabs>
        </div>
      </el-collapse-item>
      <el-collapse-item name="migRecord">
        <el-descriptions
          :column="3"
          size="small"
          border
          :label-style="{'text-align': 'center'}"
          :content-style="{'text-align': 'center'}"
        >
          <template slot="title">
            <span style="font-weight: normal">项目信息</span>
          </template>
          <template slot="extra">
            <el-button
              v-if="curMigType !== 4"
              type="warning"
              icon="el-icon-refresh-left"
              size="mini"
              :disabled="!canRollback"
              @click="rollbackOp"
            >回滚</el-button>
          </template>
          <el-descriptions-item
            v-for="item in proDescItems"
            :key="item.valueKey"
          >
            <template slot="label">
              <i :class="item.icon" />
              {{ item.label }}
            </template>
            {{ backProjectInfo[item.valueKey] }}
          </el-descriptions-item>
        </el-descriptions>
        <div style="width: 100%;height: 10px; margin: 0 auto" />
        <template slot="title">
          <span class="coll-title-span">
            迁移记录<i class="el-icon-s-order" />
          </span>
        </template>
        <el-card class="box-card">
          <el-table
            :data="migRecords"
            border
            style="width: 100%"
            max-height="580"
            header-cell-class-name="cell-class"
            cell-class-name="cell-class"
          >
            <el-table-column
              v-for="header in migTblHeader"
              :key="header.prop"
              :prop="header.prop"
              :label="header.label"
              :width="header.width"
            >
              <template slot="header">
                <span v-if="header.complexHeader" v-html="header.label" />
                <span v-else>{{ header.label }}</span>
              </template>
              <template slot-scope="scope">
                <el-badge
                  v-if="header.prop === 'migType'"
                  :value="scope.row[`${header.prop}Int`] === 1 ? '文件' :
                    scope.row[`${header.prop}Int`] === 2 ? '数据' : 'SQL'"
                  class="badge-item"
                >
                  <el-tag
                    :type="scope.row[`${header.prop}Int`] === 1 ? undefined :
                      scope.row[`${header.prop}Int`] === 2 ? 'success' : 'warning'"
                    effect="dark"
                    size="small"
                  >
                    {{ scope.row[header.prop] }}
                  </el-tag>
                </el-badge>
                <el-tooltip
                  v-else-if="header.prop === 'progress'"
                  effect="dark"
                  placement="top"
                  :content="scope.row['status'] === 0 ? '准备中' :
                    scope.row['status'] === 1 ? progressShow[scope.row.id] :
                    scope.row['status'] === 2 ? '已完成' :
                    scope.row['status'] === 3 ?'执行失败' : '已暂停'"
                >
                  <el-progress
                    :id="'progress-' + scope.row['id']"
                    :percentage="scope.row['progress']"
                    :status="scope.row['progress'] === 100.0 ? scope.row['status'] === 3? 'exception' : 'success' : undefined"
                  />
                </el-tooltip>
                <span v-else-if="header.prop === 'taskState'">
                  <el-tooltip
                    class="tooltip-item"
                    effect="dark"
                    :content="getDynamicIconClass(scope.row[`${header.prop}`])['toolTips']"
                    placement="top-start"
                  >
                    <i :class="getDynamicIconClass(scope.row[`${header.prop}`])['iconSrc']" />
                  </el-tooltip>
                </span>
                <div v-else-if="header.prop === 'operation'">
                  <el-button
                    type="text"
                    size="mini"
                    icon="el-icon-search"
                    @click="lookDetail(scope.row['id'])"
                  >日志</el-button>
                  <el-button
                    v-if="scope.row['migTypeInt'] === 1"
                    type="text"
                    size="mini"
                    :disabled="(scope.row['migTypeInt'] !== 2 && scope.row['progress'] === 100.0)
                      || (scope.row['migTypeInt'] === 2 && scope.row['rollback'] === 1)
                      || scope.row['status'] === 3"
                    :icon="scope.row['taskStateInt'] !== 5 ? 'el-icon-video-pause' : 'el-icon-video-play'"
                    @click="opClick(scope.row)"
                  >
                    {{ scope.row['taskStateInt'] !== 5 ? '暂停' : '继续' }}
                  </el-button>
                </div>
                <div v-else-if="header.prop === 'statusShow'">
                  <span
                    :style="{ color: scope.row['status'] === 0 ? '#303133'
                      : scope.row['status'] === 1 ? '#409eff'
                        : scope.row['status'] === 2 ? '#67c23a'
                          : scope.row['status'] === 3 ? '#f56c6c' : '#e6a23c', fontWeight : 'bolder'}"
                  >{{ scope.row[header.prop] }}</span>
                </div>
                <div v-else-if="header.prop === 'fileName'">
                  <span v-if="!scope.row.fileName">-</span>
                  <div v-else>
                    <p v-for="item in scope.row.fileName.split(',')" :key="item">
                      {{ item }}
                    </p>
                  </div>
                </div>
                <span v-else>{{ scope.row[header.prop] }}</span>
              </template>
            </el-table-column>
          </el-table>
        </el-card>
      </el-collapse-item>
    </el-collapse>
    <div class="extra-button-class">
      <el-checkbox v-if="!sqlConv" v-model="migImg" :disabled="isMigActing">迁移镜像</el-checkbox>
      <el-tooltip v-if="!sqlConv" style="margin: 4px" effect="dark" content="数据迁移默认方式：新极智 => 新极智" placement="top-start">
        <el-checkbox v-model="migData" :disabled="isMigActing">
          迁移数据
          <i class="el-icon-warning" style="color: #F56C6C" />
        </el-checkbox>
      </el-tooltip>
      <el-checkbox v-model="sqlConv" :disabled="isMigActing">SQL转换/导出</el-checkbox>
      <el-button
        v-if="!sqlConv"
        type="primary"
        size="mini"
        icon="el-icon-caret-right"
        :disabled="(!migData && !migImg && !sqlConv) || isMigActing"
        @click="startMigrate"
      >开始</el-button>
      <el-button
        v-else
        type="primary"
        size="mini"
        icon="el-icon-caret-right"
        :disabled="isMigActing"
        @click="startSqlConvert"
      >开始</el-button>
      <el-tooltip class="item" effect="dark" content="开启新任务" placement="top-start">
        <el-button type="primary" size="mini" icon="el-icon-plus" round @click="addNewTask" />
      </el-tooltip>
      <el-button
        type="text"
        size="mini"
        icon="iconfont icon-lishijilu"
        @click="historyRecord"
      >
        历史记录
      </el-button>
    </div>

    <LogRecord
      ref="logRecordRef"
      :multiple-search="true"
      label-width="130px"
      record-title="迁移历史记录"
      :fetch-api="fetchRecords"
      :record-visible.sync="drawerShower"
      :record-desc="recordDesc"
      :search-options="searchOptions"
      @show-latest="showMigDetail"
      @look-for-detail="showMigDetail"
    />

    <el-dialog
      title="日志详情"
      :visible.sync="logVisible"
      width="40%"
      :close-on-click-modal="false"
      append-to-body
    >
      <el-scrollbar
        ref="scrollbar"
        style="height: 500px;margin: 20px"
        @scroll="handleScroll"
      >
        <div
          ref="logContent"
          class="log-content"
          v-html="formattedLogs"
        />
      </el-scrollbar>
    </el-dialog>

    <el-dialog
      v-loading.fullscreen.lock="fullscreenLoading"
      title="环境信息"
      :visible.sync="envAliasVisible"
      width="70%"
      :close-on-click-modal="false"
      append-to-body
    >
      <EnvAliasList
        :open-dv-id="openDvId"
        :pre-env-list="preEnvList"
        :env-from-type="preEnvFromType"
        @use-preset-env="usePreEnvOp"
      />
    </el-dialog>
  </div>
</template>
<script>
import LogRecord from '@/components/LogRecord.vue'
import EnvAliasList from '@/views/toolsIntegration/imgMigration/EnvAliasList.vue'
import {
  exportSqlFile,
  getImgBatchRecords, getMigBaseInfo, getMigLogDetail, getPreEnvList, getSqlConvertLines,
  imgMigration, pauseTask,
  queryImgMigRecords, resumeTask, rollbackMigrate, sqlConvert
} from '@/api/tools'
import { getDeepValue, getDeepDefValue, updateDeepValue } from '@/utils/utils'
import CodeEditor from '@/components/CodeEditor.vue'

export default {
  name: 'ImgMigration',
  components: { CodeEditor, EnvAliasList, LogRecord },
  data() {
    return {
      activeCollName: ['connInfo', 'migRecord'],

      fullscreenLoading: false,

      infoTab: 'project_conn',

      // 基础信息面板和组件数据定义，组件参数第一层为tabName的key，第二层2个key，一个是内层div参数，第二个是form表单参数
      tabsArr: [{
        label: '项目信息',
        name: 'project_conn',
        compEls: {
          'div-els': [{
            id: 'project_info_conn',
            desc: '',
            class: 'one-div'
          }], 'form-item-els': [{
            label: '项目名称',
            prop: 'projectName',
            placeholder: '请输入项目名称',
            maxLength: 255
          }, {
            label: '活动名称',
            prop: 'activityName',
            placeholder: '请输入活动名称',
            maxLength: 255
          }, {
            label: '描述信息',
            prop: 'desc',
            placeholder: '请输入描述信息',
            type: 'textarea',
            rows: 6,
            maxLength: 4096
          }]
        }
      }, {
        label: '环境信息',
        name: 'src_dst_conn',
        tip: '说明：一般情况下，新极智镜像迁移只需要输入SSH隧道环境信息即可，<br/>常规、应用数据库和云Os数据库信息都是固定的。',
        compEls: {
          'div-els': [{
            id: 'src_conn',
            class: 'conn-div',
            desc: '源环境',
            showOld: true
          }, {
            id: 'line1_conn',
            class: 'line-div',
            desc: '',
            isDst: true
          }, {
            id: 'dst_conn',
            class: 'conn-div',
            desc: '目标环境',
            showOld: true,
            isDst: true
          }], 'common-tabs': [{
            label: '常规（镜像）',
            name: 'normal',
            isShow: true,
            showSwitch: false
          }, {
            label: '应用数据库',
            name: 'appDb',
            showSwitch: true,
            defVal: true
          }, {
            label: '云Os数据库',
            name: 'osDb',
            showSwitch: true,
            defVal: true
          }, {
            label: 'SSH隧道',
            name: 'ssh',
            isShow: true,
            showSwitch: true,
            defVal: false
          }], 'form-item-els': [{
            label: '主机',
            prop: 'host',
            placeholder: '请输入主机IP'
          }, {
            label: '端口号',
            prop: 'port',
            placeholder: '请输入端口号',
            max: 65535
          }, {
            label: '用户名',
            prop: 'username',
            placeholder: '请输入用户名',
            maxLength: 32
          }, {
            label: '密码',
            prop: 'password',
            placeholder: '请输入密码',
            maxLength: 64
          }]
        }
      }, {
        label: '镜像信息',
        name: 'list_conn',
        tip: '说明：参考镜像ID用来获取与环境强绑定的字段，' +
          '<br>如: 靶场ID(range_id)，可见性(visibility)，单位ID(company_id)等，' +
          '<br>为空时将查询库中历史数据，取第1条作为参考。',
        compEls: {
          'div-els': [{
            id: 'img_list_conn',
            desc: '',
            class: 'one-div'
          }], 'form-item-els': [{
            label: '参考镜像ID',
            prop: 'referImgId',
            placeholder: '请输入参考镜像ID',
            isRefer: true
          }, {
            label: '源文件路径',
            prop: 'srcPath',
            placeholder: '请输入源文件路径',
            maxLength: 512,
            isPath: true
          }, {
            label: '目标文件路径',
            prop: 'destPath',
            placeholder: '请输入目标文件路径',
            maxLength: 512,
            isPath: true
          }, {
            label: '镜像文件列表',
            prop: 'imgIdList',
            placeholder: '请输入镜像ID，一行一个',
            type: 'textarea',
            rows: 6
          }]
        }
      }, {
        label: 'SQL信息',
        name: 'sql_conn',
        compEls: {
          'div-els': [{
            id: 'res_conn',
            class: 'conn-div',
            desc: '原始数据',
            sqlDiv: true
          }, {
            id: 'line1_conn',
            class: 'line-div',
            desc: ''
          }, {
            id: 'tar_conn',
            class: 'conn-div',
            desc: '目标数据',
            sqlDiv: true
          }], 'form-item-els': [{
            label: 'SQL语句',
            prop: 'sqlTxt',
            placeholder: '请输入SQL语句，每行一条',
            type: 'textarea',
            rows: 15
          }]
        }
      }],

      connActiveName: {
        'src_conn': 'src_conn_ssh',
        'dst_conn': 'dst_conn_ssh'
      },

      migImg: true,
      migData: true,

      // 是否迁移中
      isMigActing: false,

      // 已经完成的任务Id列表
      hasDoneTasks: [],

      // 连接信息
      connForm: {},
      // 镜像信息和项目信息
      imgProjectInfo: { 'img_list_conn': {
        referImgId: '',
        srcPath: this.$const.IMG_NORMAL_PATH,
        destPath: this.$const.IMG_NORMAL_PATH,
        imgIdList: ''
      }, 'project_info_conn': {
        projectName: '',
        activityName: '',
        desc: '',
        envAlias: ''
      }
      },

      // 后台返回的项目信息，与前端存值的分开，避免编辑受影响
      backProjectInfo: {
        projectName: '',
        activityName: '',
        desc: ''
      },

      migTblHeader: [{
        prop: 'batchId',
        label: '批次ID'
      }, {
        prop: 'migType',
        label: '迁移类型',
        width: '150px'
      }, {
        prop: 'fileName',
        label: '对象名称'
      }, {
        prop: 'execTime',
        label: '任务开始时间',
        width: '180px'
      }, {
        prop: 'showDuration',
        label: '执行时长',
        width: '180px'
      }, {
        prop: 'fileSize',
        label: '文件大小<p style="font-size: 10px; color: #909399;margin: 0;"> 数据迁移为表个数</p>',
        width: '150px',
        complexHeader: true
      }, {
        prop: 'progress',
        label: '迁移进度'
      }, {
        prop: 'statusShow',
        label: '迁移结果',
        width: '150px'
      }, {
        prop: 'operation',
        label: '操作',
        width: '130px'
      }],
      migRecords: [],

      // 基础规则
      baseRules: {
        'conn_rules': {
          host: [{
            message: '请输入正确的IPv4地址',
            trigger: 'blur',
            pattern: /^((2[0-4]\d|25[0-5]|[01]?\d\d?)\.){3}(2[0-4]\d|25[0-5]|[01]?\d\d?)$/
          }],
          port: [{
            trigger: 'blur',
            pattern: /^([1-9](\d{0,3}))$|^([1-5]\d{4})$|^(6[0-4]\d{3})$|^(65[0-4]\d{2})$|^(655[0-2]\d)$|^(6553[0-5])$/,
            message: '端口号范围：1~65535'
          }],
          username: [{ min: 3, max: 32, message: '长度在 3 到 32 个字符', trigger: 'blur' }],
          password: []
        },
        'one_form_rules': {
          referImgId: [],
          srcPath: [],
          destPath: [],
          imgIdList: [{ required: true, message: '镜像文件列表不能为空', trigger: 'blur' }],
          projectName: [{ required: true, message: '项目名称不能为空', trigger: 'blur' }],
          activityName: [{ required: true, message: '活动名称不能为空', trigger: 'blur' }],
          envAlias: [{ required: true, message: '环境名称不能为空', trigger: 'blur' }]
        }
      },

      // 进度条显示提示信息
      progressShow: {},

      /* 历史记录相关 */
      drawerShower: false,
      recordDesc: {
        title: '项目名称：',
        props: ['projectName'],
        items: [{
          label: '批次ID',
          prop: 'batchId'
        }, {
          label: '源环境主机',
          prop: 'srcEnvHost'
        }, {
          label: '目标环境主机',
          prop: 'dstEnvHost'
        }, {
          label: '执行时间',
          prop: 'execTime'
        }, {
          label: '执行时长(秒/s)',
          prop: 'execDuration'
        }]
      },
      searchOptions: [{
        label: '项目信息',
        prop: 'proacInfo',
        type: 1,
        placeHolder: '请输入项目信息或活动信息或描述信息',
        isDefault: true
      }, {
        label: '批次ID',
        prop: 'batchId',
        type: 1,
        placeHolder: '请输入批次ID'
      }, {
        label: '源环境IP',
        prop: 'srcEvnHost',
        type: 1,
        placeHolder: '支持常规、数据库或SSH隧道环境',
        rules: [{
          message: '请输入正确的IPv4地址',
          trigger: 'blur',
          pattern: /^((2[0-4]\d|25[0-5]|[01]?\d\d?)\.){3}(2[0-4]\d|25[0-5]|[01]?\d\d?)$/
        }]
      }, {
        label: '目标环境IP',
        prop: 'dstEnvHost',
        type: 1,
        placeHolder: '支持常规、数据库或SSH隧道环境',
        rules: [{
          message: '请输入正确的IPv4地址',
          trigger: 'blur',
          pattern: /^((2[0-4]\d|25[0-5]|[01]?\d\d?)\.){3}(2[0-4]\d|25[0-5]|[01]?\d\d?)$/
        }]
      }, {
        label: '执行时间',
        prop: 'migTime',
        type: 2,
        placeHolder: '请选择时间范围',
        options: {
          shortcuts: [{
            text: '最近一周',
            onClick(picker) {
              const end = new Date()
              const start = new Date()
              start.setTime(start.getTime() - 3600 * 1000 * 24 * 7)
              picker.$emit('pick', [start, end])
            }
          }, {
            text: '最近一个月',
            onClick(picker) {
              const end = new Date()
              const start = new Date()
              start.setTime(start.getTime() - 3600 * 1000 * 24 * 30)
              picker.$emit('pick', [start, end])
            }
          }, {
            text: '最近三个月',
            onClick(picker) {
              const end = new Date()
              const start = new Date()
              start.setTime(start.getTime() - 3600 * 1000 * 24 * 90)
              picker.$emit('pick', [start, end])
            }
          }]
        }
      }, {
        label: '迁移结果',
        prop: 'status',
        type: 3,
        placeHolder: '请选择迁移结果',
        options: [{
          label: '迁移成功',
          value: 2
        }, {
          label: '迁移失败',
          value: 3
        }, {
          label: '迁移中',
          value: 1
        }]
      }],

      // 项目描述信息
      proDescItems: [{
        icon: 'el-icon-bank-card',
        label: '项目名称',
        valueKey: 'projectName'
      }, {
        icon: 'el-icon-reading',
        label: '活动名称',
        valueKey: 'activityName'
      }, {
        icon: 'el-icon-document',
        label: '描述信息',
        valueKey: 'desc'
      }],

      // 日志详情
      logVisible: false,
      logContent: '',

      // 是否允许回滚
      canRollback: true,

      // 当前迁移详情的批次ID，用于回滚操作
      curBatchId: '',
      // 当前迁移详情的迁移类型，用来控制回滚按钮的显示
      curMigType: 1,

      // 环境别名信息
      envAliasInfo: {},
      envAliasVisible: false,
      // 预置环境信息
      preEnvList: [],
      preEnvFromType: 0,
      openDvId: '',

      // sql转换
      sqlConv: false,
      sqlForm: {},
      // 靶场ID
      sqlRangeId: '',
      loadCurPage: 1,
      // 是否已经完成SQL转换数据初始化，不重复请求，后面由滚动事件请求
      hasInitLines: false,
      // 是否有更多的SQL数据
      hasMore: true
    }
  },
  computed: {
    formattedLogs() {
      return this.highLightErrors(this.logContent)
    }
  },

  watch: {
    migData: {
      immediate: true,
      handler(newVal) {
        if (!newVal) {
          const srcName = this.connActiveName['src_conn']
          const dstName = this.connActiveName['dst_conn']
          if (srcName === 'src_conn_appDb' || srcName === 'src_conn_osDb') {
            this.$set(this.connActiveName, 'src_conn', 'src_conn_normal')
          }
          if (dstName === 'dst_conn_appDb' || dstName === 'dst_conn_osDb') {
            this.$set(this.connActiveName, 'dst_conn', 'dst_conn_normal')
          }
        }
      }
    },
    migImg: {
      immediate: true,
      handler(newVal) {
        if (!newVal) {
          this.$refs['img_list_conn_formRef'][0].clearValidate()
        }
      }
    },
    sqlConv: {
      immediate: true,
      handler(newVal) {
        this.migImg = !newVal

        if (newVal) {
          if (this.infoTab === 'src_dst_conn' || this.infoTab === 'list_conn') {
            this.infoTab = 'sql_conn'
          }
          this.migData = newVal
          this.$refs['img_list_conn_formRef'][0].clearValidate()
        } else {
          this.infoTab = 'project_conn'
          this.migData = !newVal
        }
      }
    }
  },

  mounted() {
    // 内置常规和数据库的信息，一般是固定的
    this.initBaseInfo('src_conn')
    this.initBaseInfo('dst_conn')
  },
  beforeDestroy() {
    this.migSubs?.unsubscribe()
    if (this.scrollContainer) {
      this.scrollContainer.removeEventListener('scroll', this.handleCodeEditorScroll)
    }
  },
  methods: {
    getDeepDefValue,
    updateDeepValue,
    getDeepValue,

    handleScroll({ scrollTop, scrollHeight }) {
      const wrapper = this.$refs.scrollbar.wrap
      this.autoScroll = scrollHeight - (scrollTop + wrapper.clientHeight) < 50
      this.scrollTop = scrollTop
      this.scrollHeight = scrollHeight
    },

    escapeHtml(unsafeText) {
      return unsafeText
        .replace(/&/g, '&amp;')
        .replace(/</g, '&lt;')
        .replace(/>/g, '&gt;')
        .replace(/"/g, '&quot;')
        .replace(/'/g, '&#039;')
    },
    // 错误行判断逻辑
    isErrorLine(line) {
      return line.match(/(ERROR|BUILD FAILURE|FAILED|ERR|Exception)/i)
    },
    highLightErrors(content) {
      if (!content) {
        return ''
      }
      return content.split('\n').map(line => {
        // 转义 HTML 特殊字符
        const escapedLine = this.escapeHtml(line)
        // 错误检测逻辑
        if (this.isErrorLine(escapedLine)) {
          return `<span style="color: #f56c6c; font-weight: 600">${escapedLine}</span><br/>`
        }
        return `${escapedLine}<br/>`
      }).join('')
    },

    handleCodeEditorScroll: _.throttle(async function() {
      const container = this.scrollContainer
      if (!container || !this.hasMore) {
        return
      }

      // 判断是否滚动到接近底部（距离底部50px以内）
      const { scrollTop, scrollHeight, clientHeight } = container
      const isNearBottom = scrollHeight - scrollTop - clientHeight <= 50

      if (isNearBottom) {
        // 加载下一页
        this.loadCurPage += 1
        if (!this.hasMore) {
          return
        }
        const convertResp = await getSqlConvertLines({
          batchId: this.curBatchId,
          currentPage: this.loadCurPage
        })
        this.hasMore = convertResp.data.hasMore
        this.loadAppendSql('res_conn', convertResp.data.resLines)
        this.loadAppendSql('tar_conn', convertResp.data.tarLines)
      }
    }, 300), // 300ms节流间隔

    loadAppendSql(divId, lines) {
      if (lines) {
        const originContext = this.getDeepDefValue(this.sqlForm, [divId, 'sqlTxt'])
        // 每次追加时添加换行符，确保新内容另起一行
        const realContext = originContext ? `${originContext}\n${lines}` : lines
        this.updateDeepValue(this.sqlForm, [divId, 'sqlTxt'], realContext)
      }
    },

    // 获取历史记录
    fetchRecords(reqParams) {
      const migTime = reqParams['migTime']
      const cpParams = { ...reqParams }
      if (migTime && migTime.length > 0) {
        this.$set(cpParams, 'startTime', migTime[0])
        this.$set(cpParams, 'endTime', migTime[1])
        delete cpParams['migTime']
      }
      return queryImgMigRecords(cpParams)
    },

    historyRecord() {
      this.drawerShower = true
    },

    initBaseInfo(divId) {
      this.updateDeepValue(this.connForm, [divId, 'normal'], { ...this.$const.ENV_NORMAL })
      this.updateDeepValue(this.connForm, [divId, 'appDb'], { ...this.$const.ENV_APP_DB })
      this.updateDeepValue(this.connForm, [divId, 'osDb'], { ...this.$const.ENV_OS_DB })
    },

    // 获取动态的状态class值，显示为图标
    getDynamicIconClass(statusInt) {
      return statusInt === 0 ? { toolTips: '任务初始化', iconSrc: 'iconfont icon-xitongchushihua' }
        : statusInt === 1 ? { toolTips: '任务执行中', iconSrc: 'el-icon-loading' }
          : statusInt === 2 ? { toolTips: '任务执行成功', iconSrc: 'iconfont icon-bushuchenggong' }
            : statusInt === 3 ? { toolTips: '任务执行失败', iconSrc: 'iconfont icon-bushushibai' }
              : statusInt === 4 ? { toolTips: '任务等待中', iconSrc: 'iconfont icon-daizhihang' }
                : { toolTips: '任务待执行', iconSrc: 'icon-stop-circle' }
    },

    // 获取源环境和目标环境的动态form规则
    getDynamicRules(divId, tabName) {
      const rules = { ...this.baseRules['conn_rules'] }
      const nRules = {}
      if (this.isFieldRequired(divId, tabName)) {
        for (const key in rules) {
          const fRules = rules[key]
          const cRules = [...fRules]
          cRules.unshift({
            required: true,
            message: '必填项不能为空',
            trigger: 'blur'
          })
          nRules[key] = cRules
        }
        return nRules
      }

      const formRefs = this.$refs[`${divId}-${tabName}-formRef`]
      if (formRefs && formRefs.length > 0) {
        formRefs.forEach(formRef => {
          formRef.clearValidate()
        })
      }
    },

    // 获取单个Form信息的规则
    getOneFormRules() {
      const rules = { ...this.baseRules['one_form_rules'] }
      const nRules = {}
      for (const key in rules) {
        const fRules = rules[key]
        const cRules = [...fRules]
        if (this.migImg && (key === 'srcPath' || key === 'destPath')) {
          cRules.unshift({
            required: true,
            message: '必填项不能为空',
            trigger: 'blur'
          })
          nRules[key] = cRules
          continue
        }
        nRules[key] = fRules
      }
      return nRules
    },

    // 判断字段是否需要必填（根据业务逻辑）
    isFieldRequired(id, tabName) {
      return tabName === 'normal' ? true : !this.connForm[id] ? false : this.connForm[id][tabName]['enable' + tabName]
    },

    startMigrate() {
      const loading = this.$loading({
        lock: true,
        text: '正在加载数据...',
        spinner: 'el-icon-loading',
        background: 'rgba(0, 0, 0, 0.7)'
      })
      this.isMigActing = true
      // 环境数据
      const envInfos = []
      // **** 拼接数据 ****
      const srcConnEnvs = this.assembleConnForm('src_conn', 1)
      srcConnEnvs.forEach(e => { e['envAlias'] = this.getDeepDefValue(this.envAliasInfo, ['src_conn', 'envAlias']) })
      const dstConnEnvs = this.assembleConnForm('dst_conn', 2)
      dstConnEnvs.forEach(e => { e['envAlias'] = this.getDeepDefValue(this.envAliasInfo, ['dst_conn', 'envAlias']) })

      // 对环境数据进行过滤，只迁移镜像时，只需要传常规，迁移数据时，才需要db信息，根据是否开启ssh传ssh环境信息
      if (!this.migData) {
        envInfos.push(...[...srcConnEnvs.filter(item => item.envUsageType === 1 || item.envUsageType === 4),
          ...dstConnEnvs.filter(item => item.envUsageType === 1 || item.envUsageType === 4)])
      } else {
        envInfos.push(...[...srcConnEnvs, ...dstConnEnvs])
      }

      // 镜像信息
      const listConn = this.imgProjectInfo['img_list_conn']
      const reqImgInfo = {
        referImgId: listConn.referImgId,
        srcPath: listConn.srcPath,
        dstPath: listConn.destPath,
        imgIdList: listConn.imgIdList.split('\n')
      }

      // 项目信息
      const proInfo = this.imgProjectInfo['project_info_conn']
      const migType = (this.migImg && !this.migData) ? 1 : (!this.migImg && this.migData) ? 2 : 3
      const reqParam = {
        projectName: proInfo.projectName,
        activityName: proInfo.activityName,
        desc: proInfo.desc,
        envInfos: envInfos,
        imgInfo: reqImgInfo,
        migType: migType
      }

      // 校验环境别名是否为空
      if (this.getDeepDefValue(this.envAliasInfo, ['src_conn', 'envAlias']) &&
        this.getDeepDefValue(this.envAliasInfo, ['dst_conn', 'envAlias'])) {
        this.submitMigTask(loading, reqParam)
        return
      }

      this.$confirm('源环境或目标环境的别名为空，是否继续提交任务？', '任务提交确认', {
        confirmButtonText: '确认',
        cancelButtonText: '取消',
        type: 'warning'
      }).then(() => {
        this.submitMigTask(loading, reqParam)
      }).catch(() => {
        loading.close()
        this.isMigActing = false
      })
    },

    submitMigTask(loading, reqParam) {
      imgMigration(reqParam).then(res => {
        this.$message.success('任务提交成功')
        this.$refs.logRecordRef.reload()
        setTimeout(() => {
          loading.close()
        }, 1000)
      }).catch(e => {
        console.error(e)
        loading.close()
        this.isMigActing = false
      })
    },

    assembleConnForm(connDivId, envFromType) {
      const envInfos = []
      const connDiv = this.connForm[connDivId]
      // ----- 组装MigEnvInfo
      // ssh环境
      const sshTabForm = connDiv['ssh']
      if (sshTabForm['enablessh']) {
        const sshEnvInfo = this.buildMigEnvInfo(sshTabForm, 4, envFromType)
        delete sshEnvInfo.sshTunnelHost
        envInfos.push(sshEnvInfo)
      }

      // 系统环境
      const sysTabForm = connDiv['normal']
      envInfos.push(this.buildMigEnvInfo(sysTabForm, 1, envFromType))

      // 应用数据库
      const appDbForm = connDiv['appDb']
      if (appDbForm['enableappDb']) {
        envInfos.push(this.buildMigEnvInfo(appDbForm, 2, envFromType))
      }

      // 云Os数据库
      const osDbForm = connDiv['osDb']
      if (osDbForm['enableosDb']) {
        envInfos.push(this.buildMigEnvInfo(osDbForm, 3, envFromType))
      }

      return envInfos
    },

    buildMigEnvInfo(form, envUsageType, envFromType) {
      return {
        host: form.host,
        port: form.port,
        username: form.username,
        password: form.password,
        envAlias: form.envAlias,
        envUsageType: envUsageType,
        envFromType: envFromType,
        isOldEnv: form.isOldEnv
      }
    },

    // 回显迁移详情
    async showMigDetail(record) {
      const batchId = record.batchId
      this.curBatchId = batchId
      this.curMigType = record.migType

      const sRecord = await this.writeBackMigRecordDetail(batchId)

      // 任务执行中，则回填信息，并保持禁操状态
      await this.fillDoingTaskBaseInfo(sRecord, batchId)

      await this.subscribeProgress(batchId)
    },

    // 回写迁移记录详情
    async writeBackMigRecordDetail(batchId) {
      const res = await getImgBatchRecords({ batchId: batchId })
      if (!res.data) {
        return null
      }
      this.migRecords = [... res.data]
      this.migRecords.forEach(item => {
        this.$set(item, 'fileName', item.migType === 2 ? item.dataNames : item.fileName)
        this.$set(item, 'migTypeInt', item.migType)
        this.$set(item, 'migType', item.migType === 1 ? '文件迁移' : '数据迁移')
        this.$set(item, 'taskStateInt', item.taskState)
        this.$set(item, 'status', item.status)
        this.$set(item, 'statusShow', item.status === 0 ? '准备中' : item.status === 1 ? '迁移中'
          : item.status === 2 ? '迁移成功' : item.status === 3 ? '迁移失败' : '已暂停')
        this.$set(item, 'progress', Number.parseFloat(item.percent?.toFixed(1) || 0))
        this.$set(item, 'fileSize', item.totalSize)
        this.$set(item, 'rollback', item.hasRollback ? 1 : 0)
      })
      return this.migRecords[0]
    },

    // 回填执行中任务基础信息
    async fillDoingTaskBaseInfo(sRecord, batchId) {
      if (!sRecord) {
        return
      }

      // 任务执行中，保持禁止操作状态
      this.isMigActing = sRecord.taskState === 1 || sRecord.taskState === 0

      // 获取基础信息
      const baseInfoRsp = await getMigBaseInfo({ batchId: batchId })
      const envInfos = baseInfoRsp.data.envInfos
      const resImgInfo = baseInfoRsp.data.imgInfo
      const migType = baseInfoRsp.data.migType
      this.canRollback = baseInfoRsp.data.canRollback

      // 迁移类型
      if (migType === 1) {
        this.migImg = true
        this.migData = false
        this.sqlConv = false
      } else if (migType === 2) {
        this.migImg = false
        this.migData = true
        this.sqlConv = false
      } else if (migType === 4) {
        this.migImg = false
        this.migData = false
        this.sqlConv = true
      } else {
        this.migImg = true
        this.migData = true
        this.sqlConv = false
      }

      // 镜像信息和项目信息
      this.imgProjectInfo = {
        'img_list_conn': {
          referImgId: resImgInfo.referImgId,
          srcPath: resImgInfo.srcPath,
          destPath: resImgInfo.dstPath,
          imgIdList: resImgInfo.imgIdList.join('\n')
        },
        'project_info_conn': {
          projectName: baseInfoRsp.data.projectName,
          activityName: baseInfoRsp.data.activityName,
          desc: baseInfoRsp.data.desc
        }
      }

      this.backProjectInfo = {
        projectName: baseInfoRsp.data.projectName,
        activityName: baseInfoRsp.data.activityName,
        desc: baseInfoRsp.data.desc
      }

      // 环境信息
      const srcEnvs = envInfos.filter(item => item.envFromType === 1)
      const dstEnvs = envInfos.filter(item => item.envFromType === 2)
      this.fillBaseEnvInfo('src_conn', srcEnvs)
      this.fillBaseEnvInfo('dst_conn', dstEnvs)

      // sql转换信息
      if (migType === 4 && !this.hasInitLines) {
        await this.fillSqlConvertInfo()
        // 获取目标编辑器的textarea元素（参考之前的ref命名）
        this.$nextTick(() => {
          const resEditor = this.$refs[`res_conn_dataRef`]
          if (resEditor && resEditor[0].$refs.editor) {
            this.scrollContainer = resEditor[0].$refs.editor // 存储滚动容器
            // 绑定滚动事件（添加节流处理，避免频繁触发）
            this.scrollContainer.addEventListener('scroll', this.handleCodeEditorScroll)
          }
        })
        this.hasInitLines = true
      }
    },

    // 回填sql转换的基础信息
    async fillSqlConvertInfo() {
      const convertResp = await getSqlConvertLines({ batchId: this.curBatchId, currentPage: this.loadCurPage })
      const resLines = convertResp.data.resLines
      const tarLines = convertResp.data.tarLines
      if (resLines) {
        this.updateDeepValue(this.sqlForm, ['res_conn', 'sqlTxt'], resLines)
      }
      if (tarLines) {
        this.updateDeepValue(this.sqlForm, ['tar_conn', 'sqlTxt'], tarLines)
      }

      const direction = convertResp.data.direction
      if (direction === 1) {
        this.updateDeepValue(this.connForm, ['res_conn', 'normal', 'isOldEnv'], true)
        this.updateDeepValue(this.connForm, ['tar_conn', 'normal', 'isOldEnv'], true)
      } else if (direction === 2) {
        this.updateDeepValue(this.connForm, ['res_conn', 'normal', 'isOldEnv'], true)
        this.updateDeepValue(this.connForm, ['tar_conn', 'normal', 'isOldEnv'], false)
      } else if (direction === 3) {
        this.updateDeepValue(this.connForm, ['res_conn', 'normal', 'isOldEnv'], false)
        this.updateDeepValue(this.connForm, ['tar_conn', 'normal', 'isOldEnv'], false)
      } else {
        this.updateDeepValue(this.connForm, ['res_conn', 'normal', 'isOldEnv'], false)
        this.updateDeepValue(this.connForm, ['tar_conn', 'normal', 'isOldEnv'], true)
      }
    },

    // 回填基础环境信息
    fillBaseEnvInfo(divId, envInfo) {
      const normal = envInfo.find(item => item.envUsageType === 1) || { ...this.$const.ENV_NORMAL }
      const appDb = envInfo.find(item => item.envUsageType === 2) || {}
      const osDb = envInfo.find(item => item.envUsageType === 3) || {}
      const ssh = envInfo.find(item => item.envUsageType === 4) || {}

      // 回填环境别名
      this.updateDeepValue(this.envAliasInfo, [divId, 'envAlias'], normal.envAlias)

      this.updateDeepValue(this.connForm, [divId, 'normal'], normal)
      this.updateTabEnvInfo(divId, 'appDb', appDb)
      this.updateTabEnvInfo(divId, 'osDb', osDb)
      this.updateTabEnvInfo(divId, 'ssh', ssh)

      this.connActiveName[divId] = ssh.username ? divId + '_ssh' : divId + '_normal'
    },

    updateTabEnvInfo(divId, tabName, tabEnvObj) {
      if (tabEnvObj.username) {
        tabEnvObj['enable' + tabName] = true
        this.updateDeepValue(this.connForm, [divId, tabName], tabEnvObj)
      } else {
        tabEnvObj['enable' + tabName] = false
        this.updateDeepValue(this.connForm, [divId, tabName], tabEnvObj)
      }
    },

    // 订阅进度推送
    async subscribeProgress(batchId) {
      if (this.migSubs) {
        this.migSubs.unsubscribe() // 取消旧订阅
      }

      // 重置已完成ID列表
      this.hasDoneTasks = []

      // 初始化websocket
      const stompClient = await this.$store.dispatch('websocket/initWebSocket')
      this.migSubs = stompClient.subscribe('/topic/project/mig/img/progress', (message) => {
        const progress = JSON.parse(message.body)
        const taskId = progress.taskId
        const percent = progress.percent.toFixed(1) // 进度
        const speed = progress.speed // 速率
        const remainTime = progress.remainingTime // 剩余时间
        const totalSize = progress.totalSize
        const transferredSize = progress.transferred
        const serviceType = progress.serviceType
        const extraInfo = progress.extraInfo

        // 当前业务，附加信息为sql转换后的SQL语句
        if (serviceType === 3 && extraInfo) {
          this.loadAppendSql('tar_conn', extraInfo)
          // 自动滚动到底部（核心新增代码）
          this.$nextTick(() => {
            // 通过ref获取目标编辑器组件
            const targetEditor = this.$refs[`tar_conn_dataRef`]
            if (targetEditor && targetEditor[0].$refs) {
              const textarea = targetEditor[0].$refs.editor
              // 强制滚动到底部
              textarea.scrollTop = textarea.scrollHeight
            }
          })
        }

        const updatedRecords = [...this.migRecords]
        const recordIndex = updatedRecords.findIndex(r => r.id === taskId)
        if (recordIndex < 0) {
          return
        }

        // 任务执行中，保持禁止操作状态
        this.isMigActing = progress.taskState === 1

        this.progressShow[taskId] = `进度: ${percent}% | 速率: ${speed} | 已传: ${transferredSize} | 剩余时间: ${remainTime}`
        updatedRecords[recordIndex].progress = Number.parseFloat(percent)
        updatedRecords[recordIndex].fileSize = totalSize
        updatedRecords[recordIndex].status = progress.status
        updatedRecords[recordIndex].statusShow = progress.status === 0 ? '准备中'
          : progress.status === 1 ? '迁移中'
            : progress.status === 2 ? '迁移成功'
              : progress.status === 3 ? '迁移失败' : '已暂停'
        updatedRecords[recordIndex].taskState = progress.taskState
        updatedRecords[recordIndex].showDuration = progress.duration
        this.migRecords = updatedRecords

        if ((percent === '100.0' || progress.status === 3 || progress.status === 2 || progress.status === 4) &&
          this.hasDoneTasks.indexOf(taskId) === -1) {
          this.hasDoneTasks.push(taskId)
        }
        if (this.hasDoneTasks.length === this.migRecords.length) {
          this.isMigActing = false
          this.hasDoneTasks = []
        }
      }, { batchId: batchId })
    },

    // 查看迁移详情
    async lookDetail(migId) {
      this.logVisible = true
      const resp = await getMigLogDetail({ migId: migId })
      this.logContent = resp.data.result
    },

    // 点击操作，包括暂停、恢复、回退
    opClick(row) {
      const loading = this.$loading({
        lock: true,
        text: '正在加载数据...',
        spinner: 'el-icon-loading',
        background: 'rgba(0, 0, 0, 0.7)'
      })
      // 文件迁移
      if (row.taskStateInt === 5) {
        resumeTask({ taskId: row.id }).then(res => {
          if (res.data !== null) {
            this.$message.warning('恢复失败，，任务可能已被删除或当前状态不支持恢复')
          } else {
            this.$message.success('继续传输')
            this.$set(row, 'taskStateInt', 6)
          }

          this.isMigActing = true
          loading.close()
        }).catch(err => {
          console.error(err)
          this.$message.error('操作失败')
          loading.close()
        })
      } else {
        pauseTask({ taskId: row.id }).then(res => {
          if (res.data !== null) {
            this.$message.warning('暂停失败，任务可能已被删除或当前状态不支持暂停')
          } else {
            this.$message.success('暂停成功')
            this.$set(row, 'taskStateInt', 5)
          }

          loading.close()
        }).catch(err => {
          console.error(err)
          this.$message.error('操作失败')
          loading.close()
        })
      }
    },

    // 回滚操作
    rollbackOp() {
      this.$confirm('回滚操作将会删除已迁移的镜像文件和数据库的数据，是否继续？', {
        confirmButtonText: '确认',
        cancelButtonText: '取消',
        type: 'warning'
      }).then(() => {
        const loading = this.$loading({
          lock: true,
          text: '正在加载数据...',
          spinner: 'el-icon-loading',
          background: 'rgba(0, 0, 0, 0.7)'
        })
        rollbackMigrate({ batchId: this.curBatchId }).then(res => {
          this.$message.success('回滚成功')
          loading.close()
        }).catch(err => {
          console.error(err)
          this.$message.error('回滚失败')
          loading.close()
        })
      })
    },

    // 开启新任务
    addNewTask() {
      // 迁移类型
      this.migImg = true
      this.migData = true
      this.sqlConv = false

      // 迁移中标识
      this.isMigActing = false

      // 数据初始化
      this.imgProjectInfo = { 'img_list_conn': {
        referImgId: '',
        srcPath: this.$const.IMG_NORMAL_PATH,
        destPath: this.$const.IMG_NORMAL_PATH,
        imgIdList: ''
      }, 'project_info_conn': {
        projectName: '',
        activityName: '',
        desc: ''
      }
      }

      this.sqlForm = {
        'res_conn': {
          sqlTxt: ''
        },
        'tar_conn': {
          sqlTxt: ''
        }
      }

      this.updateDeepValue(this.connForm, ['res_conn', 'normal', 'isOldEnv'], false)
      this.updateDeepValue(this.connForm, ['tar_conn', 'normal', 'isOldEnv'], false)

      this.connForm = {}
      this.initBaseInfo('src_conn')
      this.initBaseInfo('dst_conn')

      // 迁移详情列表
      this.migRecords = []
    },

    // 数据库源和目标环境启用时，需同时启用，同时取消
    dbEnvEnable(dId, tabName, checkVal) {
      if (tabName === 'ssh') {
        // ssh不需要同时选中
        this.updateDeepValue(this.connForm, [dId, tabName, 'enable' + tabName], checkVal)
        return
      }
      this.updateDeepValue(this.connForm, ['src_conn', tabName, 'enable' + tabName], checkVal)
      this.updateDeepValue(this.connForm, ['dst_conn', tabName, 'enable' + tabName], checkVal)
    },

    // 源和目标数据库环境的面板同时切换
    handleEnvTabClick(tab, event) {
      const tabName = tab.name
      const strs = tabName.split('_')
      const tabShortName = strs[2]
      if (tabShortName === 'appDb' || tabShortName === 'osDb') {
        this.connActiveName['src_conn'] = 'src_conn_' + tabShortName
        this.connActiveName['dst_conn'] = 'dst_conn_' + tabShortName
      }
    },

    // 显示环境详情列表
    async showMoreEnvs(divId) {
      this.preEnvFromType = this.getDisTypeFrom(divId)
      this.envAliasVisible = true
      this.fullscreenLoading = true
      const envFromType = this.getDisTypeFrom(divId)
      const preEnvResp = await getPreEnvList({
        queryStr: '',
        envFromType: envFromType
      })
      this.preEnvList = !preEnvResp.data ? [] : preEnvResp.data
      this.openDvId = divId
      this.fullscreenLoading = false
    },

    getDisTypeFrom(divId) {
      const disTypeFrom = this.getDeepDefValue(this.envAliasInfo, [divId, 'disTypeFrom'], false)
      if (!disTypeFrom) {
        return 0
      }
      if (divId === 'src_conn') {
        // 源环境
        return 1
      }
      // 目标环境
      return 2
    },

    // 使用预置环境信息
    usePreEnvOp(envInfo, divId) {
      this.envAliasVisible = false
      this.handlePreEnvSelect(divId, envInfo)
    },

    // 加载预置环境列表
    async loadPreEnvList(queryStr, cb, divId) {
      const envFromType = this.getDisTypeFrom(divId)
      const preEnvResp = await getPreEnvList({
        queryStr: queryStr,
        envFromType: envFromType
      })
      const envList = !preEnvResp.data ? [] : preEnvResp.data
      // 环境别名相同的环境，只展示一条
      const envAliasArr = []
      const loadEnvList = []
      envList.forEach(e => {
        let envAlias = e.envAlias
        if (!envAlias) {
          envAlias = e.normal ? e.normal.host + ':' + e.normal.port
            : e.appDb ? e.appDb.host + ':' + e.appDb.port
              : e.osDb ? e.osDb.host + ':' + e.osDb.port
                : e.ssh ? e.ssh.host + ':' + e.ssh.port : '未知环境'
        }
        if (!envAliasArr.includes(envAlias)) {
          loadEnvList.push(e)
          envAliasArr.push(envAlias)
        }
      })
      cb(loadEnvList)
    },

    // 处理预置环境选中事件
    handlePreEnvSelect(divId, item) {
      // 设置环境别名
      this.updateDeepValue(this.envAliasInfo, [divId, 'envAlias'], item.envAlias)

      this.fillEnvSelect(divId, 'normal', item.normal)
      this.fillEnvSelect(divId, 'appDb', item.appDb)
      this.fillEnvSelect(divId, 'osDb', item.osDb)
      this.fillEnvSelect(divId, 'ssh', item.ssh)
    },

    // 回写环境选择
    fillEnvSelect(divId, tabName, envObj) {
      if (!envObj) {
        this.updateDeepValue(this.connForm, [divId, tabName, 'enable' + tabName], false)
        this.updateDeepValue(this.connForm, [divId, tabName, 'host'], '')
        this.updateDeepValue(this.connForm, [divId, tabName, 'port'],
          tabName === 'normal' || tabName === 'ssh' ? 22 : 3306)
        this.updateDeepValue(this.connForm, [divId, tabName, 'username'], '')
        this.updateDeepValue(this.connForm, [divId, tabName, 'password'], '')
        return
      }

      this.updateDeepValue(this.connForm, [divId, tabName, 'enable' + tabName], true)
      this.updateDeepValue(this.connForm, [divId, tabName, 'host'], envObj['host'])
      this.updateDeepValue(this.connForm, [divId, tabName, 'port'], envObj['port'])
      this.updateDeepValue(this.connForm, [divId, tabName, 'username'], envObj['username'])
      this.updateDeepValue(this.connForm, [divId, tabName, 'password'], envObj['password'])

      // 如果带ssh隧道环境，则切到对应的面板页
      if (tabName === 'ssh') {
        this.connActiveName[divId] = divId + '_ssh'
      }
    },

    // sql转换
    startSqlConvert() {
      // 靶场ID必填，先做校验，因没有使用form表单，若未填写则弹窗提示
      const rangeId = this.sqlRangeId
      if (!rangeId) {
        this.$message.warning('目标靶场ID必填！')
        return
      }

      this.updateDeepValue(this.sqlForm, ['tar_conn', 'sqlTxt'], '')

      const proInfo = this.imgProjectInfo['project_info_conn']
      // 直接转换在开始前，先做sql语句格式的校验，避免数据混乱
      const reqParams = {
        projectName: proInfo.projectName,
        activityName: proInfo.activityName,
        desc: proInfo.desc,
        migType: 4,
        resEnvType: this.getDeepDefValue(this.connForm, ['res_conn', 'normal', 'isOldEnv']) ? 1 : 2,
        tarEnvType: this.getDeepDefValue(this.connForm, ['tar_conn', 'normal', 'isOldEnv']) ? 1 : 2,
        resLines: this.getDeepDefValue(this.sqlForm, ['res_conn', 'sqlTxt']),
        rangeId: this.sqlRangeId
      }
      sqlConvert(reqParams).then(res => {
        this.$message.success('任务提交成功')
        this.$refs.logRecordRef.reload()
      }).catch(err => {
        console.error(err)
      })
    },

    // 导出sql文件
    async exportSqlFile() {
      const exportResp = await exportSqlFile({ batchId: this.curBatchId })
      const fileName = exportResp.data
      this.$remoteExport(fileName)
    }
  }
}
</script>
<style scoped>
#imgMigration {
  margin: 20px;
}

.conn-div {
  position: relative;
  width: 100%;
  margin: 0 auto;
}

.conn-container {
  display: flex;
}

.conn-container .conn-div {
  flex: 49%;
}

.conn-container .line-div {
  flex: 2%;
}

.conn-container .one-div {
  flex: 1;
}

.conn-container .trans-span {
  font-weight: bolder;
  font-size: 14px;
  width: 100px;
  flex: 0 1 auto; margin-right: auto;
}
.conn-container .el-divider--horizontal {
  margin: 15px 0 !important;
}

.coll-title-span{
  font-weight: bolder;
  font-size: 16px;
}

.extra-button-class {
  width: 300px;
  position: absolute;
  top: 30px;
  left: 255px;
  display: flex;
  gap: 30px;
  align-items: center;
}

::v-deep .cell-class {
  text-align: center;
  font-size: 14px;
}

.badge-item {
  margin-top: 10px;
  margin-right: 40px;
}

.tooltip-item {
  margin: 4px;
}

.icon-xitongchushihua {
  color: #8B8B7A;
}

.icon-bushuchenggong {
  color: #00CD66;
}

.el-icon-loading {
  color: #5093D0;
}

.icon-bushushibai {
  color: #FF7272;
}

.icon-daizhihang {
  color: #F99D3A;
}

::v-deep .el-descriptions__header {
  margin-bottom: 5px;
}

.el-autocomplete {
  width: 100% !important;
}
.el-autocomplete .el-icon-more:hover {
  cursor: pointer;
}
</style>

<style>
.env-alias-class {
  li {
    line-height: normal;
    padding: 7px;

    .name {
      text-overflow: ellipsis;
      overflow: hidden;
    }
    .addr {
      font-size: 12px;
      color: #b4b4b4;
    }

    .highlighted .addr {
      color: #ddd;
    }
  }
}

</style>
